2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "ESPurpleMSNAccount.h"
19 #import <libpurple/state.h>
21 #import <Adium/AIAccountControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIContentControllerProtocol.h>
24 #import <Adium/AIStatusControllerProtocol.h>
25 #import <Adium/AIAccount.h>
26 #import <Adium/AIHTMLDecoder.h>
27 #import <Adium/AIListContact.h>
28 #import <Adium/AIService.h>
29 #import <Adium/AIStatus.h>
30 #import <Adium/ESFileTransfer.h>
31 #import <AIUtilities/AIAttributedStringAdditions.h>
32 #import <AIUtilities/AIMutableStringAdditions.h>
33 #import <AIUtilities/AIStringAdditions.h>
34 #import <FriBidi/NSString-FBAdditions.h>
36 #import <libpurple/msn.h>
38 #define DEFAULT_MSN_PASSPORT_DOMAIN @"@hotmail.com"
39 #define SECONDS_BETWEEN_FRIENDLY_NAME_CHANGES 10
42 extern void msn_set_friendly_name(PurpleConnection *gc, const char *entry);
45 @interface ESPurpleMSNAccount (PRIVATE)
46 - (void)updateFriendlyNameAfterConnect;
47 - (void)setServersideDisplayName:(NSString *)friendlyName;
50 @implementation ESPurpleMSNAccount
53 * @brief The UID will be changed. The account has a chance to perform modifications
55 * For example, MSN adds @hotmail.com to the proposedUID and returns the new value
57 * @param proposedUID The proposed, pre-filtered UID (filtered means it has no characters invalid for this servce)
58 * @result The UID to use; the default implementation just returns proposedUID.
60 - (NSString *)accountWillSetUID:(NSString *)proposedUID
64 if (([proposedUID length] > 0) &&
65 ([proposedUID rangeOfString:@"@"].location == NSNotFound)) {
66 correctUID = [proposedUID stringByAppendingString:DEFAULT_MSN_PASSPORT_DOMAIN];
68 correctUID = proposedUID;
77 lastFriendlyNameChange = nil;
79 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_MSN_SERVICE];
83 [[adium preferenceController] unregisterPreferenceObserver:self];
85 [lastFriendlyNameChange release];
86 [queuedFriendlyName release];
91 - (const char*)protocolPlugin
96 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
98 NSString *msg = [super encodedAttributedStringForSendingContentMessage:inContentMessage];
101 /* If our message contains RTL string we shall surround it with a span tag
102 * with proper dir attribute so libpurple can apply the MSN writing direction
103 * flag. Note that we must check the string of the content message and not the
104 * one returned by our superclass as it appends its own html to the string.
105 * Only the content message can tell us the original direction.
107 return (([[[inContentMessage message] string] baseWritingDirection] == NSWritingDirectionRightToLeft)
108 ? [NSString stringWithFormat:@"<span dir=\"rtl\">%@</span>", msg]
115 #pragma mark Connection
116 - (void)configurePurpleAccount
118 [super configurePurpleAccount];
120 BOOL HTTPConnect = [[self preferenceForKey:KEY_MSN_HTTP_CONNECT_METHOD group:GROUP_ACCOUNT_STATUS] boolValue];
121 purple_account_set_bool(account, "http_method", HTTPConnect);
124 - (NSString *)connectionStringForStep:(int)step
130 return AILocalizedString(@"Connecting",nil);
133 return AILocalizedString(@"Handshaking",nil);
136 return AILocalizedString(@"Transferring",nil);
139 return AILocalizedString(@"Handshaking",nil);
142 return AILocalizedString(@"Starting authentication",nil);
145 return AILocalizedString(@"Getting Cookie",nil);
148 return AILocalizedString(@"Authenticating",nil);
151 return AILocalizedString(@"Sending Cookie",nil);
154 return AILocalizedString(@"Retrieving buddy list",nil);
158 return AILocalizedString(@"Connecting",nil);
161 return AILocalizedString(@"Connecting",nil);
164 return AILocalizedString(@"Syncing with server",nil);
167 return AILocalizedString(@"Requesting to send password",nil);
170 return AILocalizedString(@"Syncing with server",nil);
173 return AILocalizedString(@"Requesting to send password",nil);
176 return AILocalizedString(@"Password sent",nil);
179 return AILocalizedString(@"Retrieving buddy list",nil);
186 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
188 return [AIHTMLDecoder encodeHTML:inAttributedString
191 includingColorTags:YES
194 closeStyleTagsOnFontChange:YES
198 attachmentsAsText:YES
199 onlyIncludeOutgoingImages:NO
202 allowJavascriptURLs:YES];
205 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
207 /* If we're sending a message on an encryption chat, we can encode the HTML normally, as links will go through fine.
208 * If we're sending a message normally, MSN will drop the title of any link, so we preprocess it to be in the form "title (link)"
210 return [AIHTMLDecoder encodeHTML:([[inContentMessage chat] isSecure] ? inAttributedString : [inAttributedString attributedStringByConvertingLinksToStrings])
213 includingColorTags:YES
216 closeStyleTagsOnFontChange:YES
220 attachmentsAsText:YES
221 onlyIncludeOutgoingImages:NO
224 allowJavascriptURLs:YES];
228 //Update our full name on connect
229 - (oneway void)accountConnectionConnected
231 [super accountConnectionConnected];
233 [self updateFriendlyNameAfterConnect];
237 * @brief Update our friendly name to match the server friendly name if appropriate
239 * Well behaved MSN clients respect the serverside display name so that an update on one client is reflected on another.
241 * If our display name is static and specified specifically for our account, we should update to the serverside one if they aren't the same.
243 * However, if our display name is dynamic, most likely we're looking at the filtered version of our dynamic
244 * name, so we shouldn't update to the filtered one. Furthermore, if our display name is set at the Aduim-global level,
245 * we should use that name, not whatever is specified by the last client to connect.
247 - (void)updateFriendlyNameAfterConnect
249 const char *displayName = purple_connection_get_display_name(purple_account_get_connection(account));
250 NSAttributedString *accountDisplayName = [[self preferenceForKey:KEY_ACCOUNT_DISPLAY_NAME
251 group:GROUP_ACCOUNT_STATUS
252 ignoreInheritedValues:YES] attributedString];
253 NSAttributedString *globalPreference = [[self preferenceForKey:KEY_ACCOUNT_DISPLAY_NAME
254 group:GROUP_ACCOUNT_STATUS
255 ignoreInheritedValues:NO] attributedString];
256 BOOL accountDisplayNameChanged = NO;
257 BOOL shouldUpdateDisplayNameImmediately= NO;
259 /* If the friendly name changed since the last time we connected (the user changed it while offline)
260 * set it serverside and clear the flag.
262 if ((accountDisplayName && (accountDisplayNameChanged = [[self preferenceForKey:KEY_MSN_DISPLAY_NAMED_CHANGED group:GROUP_ACCOUNT_STATUS] boolValue])) ||
263 (!accountDisplayName && globalPreference)) {
264 shouldUpdateDisplayNameImmediately = YES;
266 if (accountDisplayNameChanged) {
267 [self setPreference:nil
268 forKey:KEY_MSN_DISPLAY_NAMED_CHANGED
269 group:GROUP_ACCOUNT_STATUS];
273 /* If our locally set friendly name didn't change since the last time we connected but one is set,
274 * we want to update to the serverside settings as appropriate.
276 * An important exception is if our per-account display name is dynamic (i.e. a 'Now Playing in iTunes' name).
278 * We explicitly ignore any display name starting with "<msnobj" because purple_connection_get_display_name() occassionaly (rarely)
279 * returns invalid data starting with that string. The user can still set this as an MSN display name if she is really that weird, but
280 * we won't update to match other clients setting it.
283 strncmp(displayName, "<msnobj", 7),
284 strcmp(displayName, [[self UID] UTF8String]) &&
285 strcmp(displayName, [[self formattedUID] UTF8String])) {
286 /* There is a serverside display name, and it's not the same as our UID. */
287 const char *accountDisplayNameUTF8String = [[accountDisplayName string] UTF8String];
289 if (accountDisplayNameUTF8String &&
290 strcmp(accountDisplayNameUTF8String, displayName)) {
291 /* The display name is different from our per-account preference, which exists. Check if our preference is static.
292 * If the if() above got FALSE, we don't need to do anything; the serverside preference should stand as-is. */
293 [[adium contentController] filterAttributedString:accountDisplayName
294 usingFilterType:AIFilterContent
295 direction:AIFilterOutgoing
298 selector:@selector(gotFilteredFriendlyName:context:)
299 context:[NSDictionary dictionaryWithObjectsAndKeys:
300 accountDisplayName, @"accountDisplayName",
301 [NSString stringWithUTF8String:displayName], @"displayName",
306 shouldUpdateDisplayNameImmediately = YES;
310 if (shouldUpdateDisplayNameImmediately) {
311 [self updateStatusForKey:KEY_ACCOUNT_DISPLAY_NAME];
315 - (void)gotFilteredFriendlyName:(NSAttributedString *)filteredFriendlyName context:(NSDictionary *)infoDict
317 if ((!filteredFriendlyName && [infoDict objectForKey:@"displayName"]) ||
318 ([[filteredFriendlyName string] isEqualToString:[[infoDict objectForKey:@"accountDisplayName"] string]])) {
319 /* Filtering made no changes to the string, so we're static. If we make it here, update to match the server. */
320 NSAttributedString *newPreference;
322 newPreference = [[NSAttributedString alloc] initWithString:[infoDict objectForKey:@"displayName"]];
324 [self setPreference:[newPreference dataRepresentation]
325 forKey:KEY_ACCOUNT_DISPLAY_NAME
326 group:GROUP_ACCOUNT_STATUS];
327 [newPreference release];
329 [self updateStatusForKey:KEY_ACCOUNT_DISPLAY_NAME];
333 [self setServersideDisplayName:[filteredFriendlyName string]];
337 - (void)doQueuedSetServersideDisplayName
339 [self setServersideDisplayName:queuedFriendlyName];
340 [queuedFriendlyName release]; queuedFriendlyName = nil;
343 - (void)setServersideDisplayName:(NSString *)friendlyName
345 if (account && purple_account_is_connected(account)) {
346 NSDate *now = [NSDate date];
348 if (!lastFriendlyNameChange ||
349 [now timeIntervalSinceDate:lastFriendlyNameChange] > SECONDS_BETWEEN_FRIENDLY_NAME_CHANGES) {
351 //Don't allow newlines in the friendly name; convert them to slashes.
352 NSMutableString *noNewlinesFriendlyName = [[friendlyName mutableCopy] autorelease];
353 [noNewlinesFriendlyName convertNewlinesToSlashes];
354 friendlyName = noNewlinesFriendlyName;
357 * The MSN display name will be URL encoded via purple_url_encode(). The maximum length of the _encoded_ string is
358 * BUDDY_ALIAS_MAXLEN (387 characters as of purple 2.0.0). We can't simply encode and truncate as we might end up with
359 * part of an encoded character being cut off, so we instead truncate to smaller and smaller strings and encode, until it fits
361 const char *friendlyNameUTF8String = [friendlyName UTF8String];
362 int currentMaxNumberOfPreEncodedCharacters = BUDDY_ALIAS_MAXLEN;
364 while (friendlyNameUTF8String &&
365 strlen(purple_url_encode(friendlyNameUTF8String)) > BUDDY_ALIAS_MAXLEN) {
366 AILog(@"Shortening because %s (max len %i) [%s] len (%i) > %i",
367 friendlyNameUTF8String, currentMaxNumberOfPreEncodedCharacters,
368 purple_url_encode(friendlyNameUTF8String),strlen(purple_url_encode(friendlyNameUTF8String)),
370 friendlyName = [noNewlinesFriendlyName stringWithEllipsisByTruncatingToLength:currentMaxNumberOfPreEncodedCharacters];
371 friendlyNameUTF8String = [friendlyName UTF8String];
372 currentMaxNumberOfPreEncodedCharacters -= 10;
375 msn_act_id(purple_account_get_connection(account), friendlyNameUTF8String);
377 msn_set_friendly_name(purple_account_get_connection(account), friendlyNameUTF8String);
380 [lastFriendlyNameChange release];
381 lastFriendlyNameChange = [now retain];
384 [NSObject cancelPreviousPerformRequestsWithTarget:self
385 selector:@selector(doQueuedSetServersideDisplayName)
387 if (queuedFriendlyName != friendlyName) {
388 [queuedFriendlyName release];
389 queuedFriendlyName = [friendlyName retain];
391 [self performSelector:@selector(doQueuedSetServersideDisplayName)
393 afterDelay:(SECONDS_BETWEEN_FRIENDLY_NAME_CHANGES - [now timeIntervalSinceDate:lastFriendlyNameChange])];
395 AILog(@"%@: Queueing serverside display name change to %@ for %0f seconds", self, queuedFriendlyName, (SECONDS_BETWEEN_FRIENDLY_NAME_CHANGES - [now timeIntervalSinceDate:lastFriendlyNameChange]));
401 * @brief Set our serverside 'friendly name'
403 * There is a rate limit on how quickly we can set our friendly name.
405 * @param attributedFriendlyName The new friendly name. This is used as plaintext; it is an NSAttributedString for generic useage with the autoupdating filtering system.
408 - (void)gotFilteredDisplayName:(NSAttributedString *)attributedDisplayName
410 NSString *friendlyName = [attributedDisplayName string];
411 AILog(@"%@: gotFilteredDisplayName: %@ (I am currently %@)",self,friendlyName,[self currentDisplayName]);
413 if (!friendlyName || ![friendlyName isEqualToString:[self currentDisplayName]]) {
414 [self setServersideDisplayName:friendlyName];
417 [super gotFilteredDisplayName:attributedDisplayName];
420 - (BOOL)useDisplayNameAsStatusMessage
422 return displayNamesAsStatus;
425 - (void)updateMobileStatus:(AIListContact *)theContact withData:(BOOL)isMobile
430 - (BOOL)shouldIncludeNowPlayingInformationInAllStatuses
432 return [[self preferenceForKey:KEY_BROADCAST_MUSIC_INFO group:GROUP_ACCOUNT_STATUS] boolValue];
435 #pragma mark File transfer
436 - (BOOL)canSendFolders
441 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
443 [super _beginSendOfFileTransfer:fileTransfer];
446 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
448 [super acceptFileTransferRequest:fileTransfer];
451 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
453 [super rejectFileReceiveRequest:fileTransfer];
456 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
458 [super cancelFileTransfer:fileTransfer];
461 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
462 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
464 [super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
466 if ([group isEqualToString:PREF_GROUP_MSN_SERVICE]) {
467 displayNamesAsStatus = [[prefDict objectForKey:KEY_MSN_DISPLAY_NAMES_AS_STATUS] boolValue];
471 #pragma mark Status messages
473 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
475 NSString *statusName = nil;
476 PurplePresence *presence = purple_buddy_get_presence(buddy);
477 PurpleStatus *status = purple_presence_get_active_status(presence);
478 const char *purpleStatusID = purple_status_get_id(status);
480 if (!purpleStatusID) return nil;
482 if (!strcmp(purpleStatusID, "brb")) {
483 statusName = STATUS_NAME_BRB;
485 } else if (!strcmp(purpleStatusID, "busy")) {
486 statusName = STATUS_NAME_BUSY;
488 } else if (!strcmp(purpleStatusID, "phone")) {
489 statusName = STATUS_NAME_PHONE;
491 } else if (!strcmp(purpleStatusID, "lunch")) {
492 statusName = STATUS_NAME_LUNCH;
494 } else if (!strcmp(purpleStatusID, "invisible")) {
495 statusName = STATUS_NAME_INVISIBLE;
502 * @brief Return the purple status ID to be used for a status
504 * Most subclasses should override this method; these generic values may be appropriate for others.
506 * Active services provided nonlocalized status names. An AIStatus is passed to this method along with a pointer
507 * to the status message. This method should handle any status whose statusNname this service set as well as any statusName
508 * defined in AIStatusController.h (which will correspond to the services handled by Adium by default).
509 * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
510 * [statusState statusType] for a general idea of the status's type.
512 * @param statusState The status for which to find the purple status ID
513 * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
515 * @result The purple status ID
517 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
518 arguments:(NSMutableDictionary *)arguments
520 const char *statusID = NULL;
521 NSString *statusName = [statusState statusName];
522 NSString *statusMessageString = [statusState statusMessageString];
524 if (!statusMessageString) statusMessageString = @"";
526 switch ([statusState statusType]) {
527 case AIAvailableStatusType:
530 case AIAwayStatusType:
531 if (([statusName isEqualToString:STATUS_NAME_BRB]) ||
532 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_BRB]] == NSOrderedSame))
534 else if (([statusName isEqualToString:STATUS_NAME_BUSY]) ||
535 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_BUSY]] == NSOrderedSame))
537 else if (([statusName isEqualToString:STATUS_NAME_PHONE]) ||
538 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_PHONE]] == NSOrderedSame))
540 else if (([statusName isEqualToString:STATUS_NAME_LUNCH]) ||
541 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_LUNCH]] == NSOrderedSame))
546 case AIInvisibleStatusType:
547 case AIOfflineStatusType:
551 //If we didn't get a purple status ID, request one from super
552 if (statusID == NULL) statusID = [super purpleStatusIDForStatus:statusState arguments:arguments];
557 #pragma mark Contact List Menu Items
558 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
560 if ((strcmp(label, _("Initiate Chat")) == 0) || (strcmp(label, _("Initiate _Chat")) == 0)) {
561 return [NSString stringWithFormat:AILocalizedString(@"Initiate Multiuser Chat with %@",nil),[inContact formattedUID]];
563 } else if (strcmp(label, _("Send to Mobile")) == 0) {
564 return [NSString stringWithFormat:AILocalizedString(@"Send to %@'s Mobile",nil),[inContact formattedUID]];
567 return [super titleForContactMenuLabel:label forContact:inContact];
570 #pragma mark Account Action Menu Items
571 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
573 if (strcmp(label, _("Set Friendly Name...")) == 0) {
574 /* Don't include the Set Friendly Name action since we have our own UI for this */
577 } else if (strcmp(label, _("Set Home Phone Number...")) == 0) {
578 return [AILocalizedString(@"Set Home Phone Number",nil) stringByAppendingEllipsis];
580 } else if (strcmp(label, _("Set Work Phone Number...")) == 0) {
581 return [AILocalizedString(@"Set Work Phone Number",nil) stringByAppendingEllipsis];
583 } else if (strcmp(label, _("Set Mobile Phone Number...")) == 0) {
584 return [AILocalizedString(@"Set Mobile Phone Number",nil) stringByAppendingEllipsis];
586 } else if (strcmp(label, _("Allow/Disallow Mobile Pages...")) == 0) {
587 return [AILocalizedString(@"Allow/Disallow Mobile Pages","Action menu item for MSN accounts to toggle whether Mobile pages [forwarding messages to a mobile device] are enabled") stringByAppendingEllipsis];
591 return [super titleForAccountActionMenuLabel:label];